 /*******************************************************************************
  * Copyright (c) 2000, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.ui.console;

 import java.util.ArrayList ;
 import java.util.List ;

 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.jface.preference.JFacePreferences;
 import org.eclipse.jface.resource.ColorRegistry;
 import org.eclipse.jface.resource.JFaceColors;
 import org.eclipse.jface.resource.JFaceResources;
 import org.eclipse.jface.text.BadPositionCategoryException;
 import org.eclipse.jface.text.DocumentEvent;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IDocumentAdapter;
 import org.eclipse.jface.text.IDocumentListener;
 import org.eclipse.jface.text.IPositionUpdater;
 import org.eclipse.jface.text.IRegion;
 import org.eclipse.jface.text.Position;
 import org.eclipse.jface.text.source.SourceViewer;
 import org.eclipse.jface.util.IPropertyChangeListener;
 import org.eclipse.jface.util.PropertyChangeEvent;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.LineBackgroundEvent;
 import org.eclipse.swt.custom.LineBackgroundListener;
 import org.eclipse.swt.custom.LineStyleEvent;
 import org.eclipse.swt.custom.LineStyleListener;
 import org.eclipse.swt.custom.StyleRange;
 import org.eclipse.swt.custom.StyledText;
 import org.eclipse.swt.events.MouseEvent;
 import org.eclipse.swt.events.MouseListener;
 import org.eclipse.swt.events.MouseMoveListener;
 import org.eclipse.swt.events.MouseTrackListener;
 import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.graphics.Cursor;
 import org.eclipse.swt.graphics.Font;
 import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Listener;
 import org.eclipse.ui.internal.console.ConsoleDocumentAdapter;
 import org.eclipse.ui.internal.console.ConsoleHyperlinkPosition;
 import org.eclipse.ui.progress.WorkbenchJob;

 /**
  * Default viewer used to display a <code>TextConsole</code>.
  * <p>
  * Clients may subclass this class.
  * </p>
  *
  * @since 3.1
  */
 public class TextConsoleViewer extends SourceViewer implements LineStyleListener, LineBackgroundListener, MouseTrackListener, MouseMoveListener, MouseListener {
     /**
      * Adapts document to the text widget.
      */
     private ConsoleDocumentAdapter documentAdapter;

     private IHyperlink hyperlink;

     private Cursor handCursor;

     private Cursor textCursor;

     private int consoleWidth = -1;

     private TextConsole console;

     private IPropertyChangeListener propertyChangeListener;

     private IDocumentListener documentListener = new IDocumentListener() {
         public void documentAboutToBeChanged(DocumentEvent event) {
         }

         public void documentChanged(DocumentEvent event) {
             updateLinks(event.fOffset);
         }
     };
     // event listener used to send event to hyperlink for IHyperlink2
 private Listener mouseUpListener = new Listener() {
         public void handleEvent(Event event) {
             if (hyperlink != null) {
                 String selection = getTextWidget().getSelectionText();
                 if (selection.length() <= 0) {
                     if (event.button == 1) {
                         if (hyperlink instanceof IHyperlink2) {
                             ((IHyperlink2) hyperlink).linkActivated(event);
                         } else {
                             hyperlink.linkActivated();
                         }
                     }
                 }
             }
         }
     };

     WorkbenchJob revealJob = new WorkbenchJob("Reveal End of Document") {//$NON-NLS-1$
 public IStatus runInUIThread(IProgressMonitor monitor) {
             StyledText textWidget = getTextWidget();
             if (textWidget != null && !textWidget.isDisposed()) {
                 int lineCount = textWidget.getLineCount();
                 textWidget.setTopIndex(lineCount - 1);
             }
             return Status.OK_STATUS;
         }
     };
     
     private IPositionUpdater positionUpdater = new IPositionUpdater() {
         public void update(DocumentEvent event) {
             try {
                 IDocument document = getDocument();
                 if (document != null) {
                     Position[] positions = document.getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
                     for (int i = 0; i < positions.length; i++) {
                         Position position = positions[i];
                         if (position.offset == event.fOffset && position.length<=event.fLength) {
                             position.delete();
                         }
                         if (position.isDeleted) {
                             document.removePosition(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY, position);
                         }
                     }
                 }
             } catch (BadPositionCategoryException e) {
             }
         }
     };

     /**
      * Constructs a new viewer in the given parent for the specified console.
      *
      * @param parent
      * containing widget
      * @param console
      * text console
      */
     public TextConsoleViewer(Composite parent, TextConsole console) {
         super(parent, null, SWT.V_SCROLL | SWT.H_SCROLL);
         this.console = console;

         IDocument document = console.getDocument();
         setDocument(document);

         StyledText styledText = getTextWidget();
         styledText.setDoubleClickEnabled(true);
         styledText.addLineStyleListener(this);
         styledText.addLineBackgroundListener(this);
         styledText.setEditable(true);
         setFont(console.getFont());
         styledText.addMouseTrackListener(this);
         styledText.addListener(SWT.MouseUp, mouseUpListener);

         ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
         propertyChangeListener = new HyperlinkColorChangeListener();
         colorRegistry.addListener(propertyChangeListener);

         revealJob.setSystem(true);
         document.addDocumentListener(documentListener);
         document.addPositionUpdater(positionUpdater);
     }

     /**
      * Sets the tab width used by this viewer.
      *
      * @param tabWidth
      * the tab width used by this viewer
      */
     public void setTabWidth(int tabWidth) {
         StyledText styledText = getTextWidget();
         int oldWidth = styledText.getTabs();
         if (tabWidth != oldWidth) {
             styledText.setTabs(tabWidth);
         }
     }

     /**
      * Sets the font used by this viewer.
      *
      * @param font
      * the font used by this viewer
      */
     public void setFont(Font font) {
         StyledText styledText = getTextWidget();
         Font oldFont = styledText.getFont();
         if (oldFont == font) {
             return;
         }
         if (font == null || !(font.equals(oldFont))) {
             styledText.setFont(font);
         }
     }

     /**
      * Positions the cursor at the end of the document.
      */
     protected void revealEndOfDocument() {
         revealJob.schedule(50);
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.custom.LineStyleListener#lineGetStyle(org.eclipse.swt.custom.LineStyleEvent)
      */
     public void lineGetStyle(LineStyleEvent event) {
         IDocument document = getDocument();
         if (document != null && document.getLength() > 0) {
             ArrayList ranges = new ArrayList ();
             int offset = event.lineOffset;
             int length = event.lineText.length();

             StyleRange[] partitionerStyles = ((IConsoleDocumentPartitioner) document.getDocumentPartitioner()).getStyleRanges(event.lineOffset, event.lineText.length());
             if (partitionerStyles != null) {
                 for (int i = 0; i < partitionerStyles.length; i++) {
                     ranges.add(partitionerStyles[i]);
                 }
             } else {
                 ranges.add(new StyleRange(offset, length, null, null));
             }

             try {
                 Position[] positions = getDocument().getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
                 Position[] overlap = findPosition(offset, length, positions);
                 Color color = JFaceColors.getHyperlinkText(Display.getCurrent());
                 if (overlap != null) {
                     for (int i = 0; i < overlap.length; i++) {
                         Position position = overlap[i];
                         StyleRange linkRange = new StyleRange(position.offset, position.length, color, null);
                         linkRange.underline = true;
                         override(ranges, linkRange);
                     }
                 }
             } catch (BadPositionCategoryException e) {
             }

             if (ranges.size() > 0) {
                 event.styles = (StyleRange[]) ranges.toArray(new StyleRange[ranges.size()]);
             }
         }
     }

     private void override(List ranges, StyleRange newRange) {
         if (ranges.isEmpty()) {
             ranges.add(newRange);
             return;
         }

         int start = newRange.start;
         int end = start + newRange.length;
         for (int i = 0; i < ranges.size(); i++) {
             StyleRange existingRange = (StyleRange) ranges.get(i);
             int rEnd = existingRange.start + existingRange.length;
             if (end <= existingRange.start || start >= rEnd) {
                 continue;
             }

             if (start < existingRange.start && end > existingRange.start) {
                 start = existingRange.start;
             }

             if (start >= existingRange.start && end <= rEnd) {
                 existingRange.length = start - existingRange.start;
                 ranges.add(++i, newRange);
                 if (end != rEnd) {
                     ranges.add(++i, new StyleRange(end, rEnd - end - 1, existingRange.foreground, existingRange.background));
                 }
                 return;
             } else if (start >= existingRange.start && start < rEnd) {
                 existingRange.length = start - existingRange.start;
                 ranges.add(++i, newRange);
             } else if (end >= rEnd) {
                 ranges.remove(i);
             } else {
                 ranges.add(++i, new StyleRange(end + 1, rEnd - end + 1, existingRange.foreground, existingRange.background));
             }
         }
     }

     /**
      * Binary search for the positions overlapping the given range
      *
      * @param offset
      * the offset of the range
      * @param length
      * the length of the range
      * @param positions
      * the positions to search
      * @return the positions overlapping the given range, or <code>null</code>
      */
     private Position[] findPosition(int offset, int length, Position[] positions) {

         if (positions.length == 0)
             return null;

         int rangeEnd = offset + length;
         int left = 0;
         int right = positions.length - 1;
         int mid = 0;
         Position position = null;

         while (left < right) {

             mid = (left + right) / 2;

             position = positions[mid];
             if (rangeEnd < position.getOffset()) {
                 if (left == mid)
                     right = left;
                 else
                     right = mid - 1;
             } else if (offset > (position.getOffset() + position.getLength() - 1)) {
                 if (right == mid)
                     left = right;
                 else
                     left = mid + 1;
             } else {
                 left = right = mid;
             }
         }

         List list = new ArrayList ();
         int index = left - 1;
         if (index >= 0) {
             position = positions[index];
             while (index >= 0 && (position.getOffset() + position.getLength()) > offset) {
                 index--;
                 if (index > 0) {
                     position = positions[index];
                 }
             }
         }
         index++;
         position = positions[index];
         while (index < positions.length && (position.getOffset() < rangeEnd)) {
             list.add(position);
             index++;
             if (index < positions.length) {
                 position = positions[index];
             }
         }

         if (list.isEmpty()) {
             return null;
         }
         return (Position[]) list.toArray(new Position[list.size()]);
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.custom.LineBackgroundListener#lineGetBackground(org.eclipse.swt.custom.LineBackgroundEvent)
      */
     public void lineGetBackground(LineBackgroundEvent event) {
         event.lineBackground = null;
     }

     /**
      * Returns the hand cursor.
      *
      * @return the hand cursor
      */
     protected Cursor getHandCursor() {
         if (handCursor == null) {
             handCursor = new Cursor(ConsolePlugin.getStandardDisplay(), SWT.CURSOR_HAND);
         }
         return handCursor;
     }

     /**
      * Returns the text cursor.
      *
      * @return the text cursor
      */
     protected Cursor getTextCursor() {
         if (textCursor == null) {
             textCursor = new Cursor(ConsolePlugin.getStandardDisplay(), SWT.CURSOR_IBEAM);
         }
         return textCursor;
     }

     /**
      * Notification a hyperlink has been entered.
      *
      * @param link
      * the link that was entered
      */
     protected void linkEntered(IHyperlink link) {
         Control control = getTextWidget();
         if (hyperlink != null) {
             linkExited(hyperlink);
         }
         hyperlink = link;
         hyperlink.linkEntered();
         control.setCursor(getHandCursor());
         control.redraw();
         control.addMouseListener(this);
     }

     /**
      * Notification a link was exited.
      *
      * @param link
      * the link that was exited
      */
     protected void linkExited(IHyperlink link) {
         link.linkExited();
         hyperlink = null;
         Control control = getTextWidget();
         control.setCursor(getTextCursor());
         control.redraw();
         control.removeMouseListener(this);
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.events.MouseTrackListener#mouseEnter(org.eclipse.swt.events.MouseEvent)
      */
     public void mouseEnter(MouseEvent e) {
         getTextWidget().addMouseMoveListener(this);
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.events.MouseTrackListener#mouseExit(org.eclipse.swt.events.MouseEvent)
      */
     public void mouseExit(MouseEvent e) {
         getTextWidget().removeMouseMoveListener(this);
         if (hyperlink != null) {
             linkExited(hyperlink);
         }
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.events.MouseTrackListener#mouseHover(org.eclipse.swt.events.MouseEvent)
      */
     public void mouseHover(MouseEvent e) {
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.events.MouseMoveListener#mouseMove(org.eclipse.swt.events.MouseEvent)
      */
     public void mouseMove(MouseEvent e) {
         int offset = -1;
         try {
             Point p = new Point(e.x, e.y);
             offset = getTextWidget().getOffsetAtLocation(p);
         } catch (IllegalArgumentException ex) {
             // out of the document range
 }
         updateLinks(offset);
     }

     /**
      * The cursor has just be moved to the given offset, the mouse has hovered
      * over the given offset. Update link rendering.
      *
      * @param offset
      */
     protected void updateLinks(int offset) {
         if (offset >= 0) {
             IHyperlink link = getHyperlink(offset);
             if (link != null) {
                 if (link.equals(hyperlink)) {
                     return;
                 }
                 linkEntered(link);
                 return;
             }
         }
         if (hyperlink != null) {
             linkExited(hyperlink);
         }
     }

     /**
      * Returns the currently active hyperlink or <code>null</code> if none.
      *
      * @return the currently active hyperlink or <code>null</code> if none
      */
     public IHyperlink getHyperlink() {
         return hyperlink;
     }

     /**
      * Returns the hyperlink at the specified offset, or <code>null</code> if
      * none.
      *
      * @param offset
      * offset at which a hyperlink has been requested
      * @return hyperlink at the specified offset, or <code>null</code> if none
      */
     public IHyperlink getHyperlink(int offset) {
         if (offset >= 0 && console != null) {
             return console.getHyperlink(offset);
         }
         return null;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.events.MouseListener#mouseDoubleClick(org.eclipse.swt.events.MouseEvent)
      */
     public void mouseDoubleClick(MouseEvent e) {
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
      */
     public void mouseDown(MouseEvent e) {
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
      */
     public void mouseUp(MouseEvent e) {
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.jface.text.TextViewer#createDocumentAdapter()
      */
     protected IDocumentAdapter createDocumentAdapter() {
         if (documentAdapter == null) {
             documentAdapter = new ConsoleDocumentAdapter(consoleWidth = -1);
         }
         return documentAdapter;
     }

     /**
      * Sets the console to have a fixed character width. Use -1 to indicate that
      * a fixed width should not be used.
      *
      * @param width
      * fixed character width of the console, or -1
      */
     public void setConsoleWidth(int width) {
         if (consoleWidth != width) {
             consoleWidth = width;
             ConsolePlugin.getStandardDisplay().asyncExec(new Runnable () {
                 public void run() {
                     if (documentAdapter != null) {
                         documentAdapter.setWidth(consoleWidth);
                     }
                 }
             });
         }
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.jface.text.TextViewer#handleDispose()
      */
     protected void handleDispose() {
         IDocument document = getDocument();
         if (document != null) {
             document.removeDocumentListener(documentListener);
             document.removePositionUpdater(positionUpdater);
         }

         StyledText styledText = getTextWidget();
         styledText.removeLineStyleListener(this);
         styledText.removeLineBackgroundListener(this);
         styledText.removeMouseTrackListener(this);

         handCursor = null;
         textCursor = null;
         hyperlink = null;
         console = null;

         ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
         colorRegistry.removeListener(propertyChangeListener);
         
         super.handleDispose();
     }

     class HyperlinkColorChangeListener implements IPropertyChangeListener {
         public void propertyChange(PropertyChangeEvent event) {
             if (event.getProperty().equals(JFacePreferences.ACTIVE_HYPERLINK_COLOR) || event.getProperty().equals(JFacePreferences.HYPERLINK_COLOR)) {
                 getTextWidget().redraw();
             }
         }

     }

     /*
      * work around to memory leak in TextViewer$WidgetCommand
      */
     protected void updateTextListeners(WidgetCommand cmd) {
         super.updateTextListeners(cmd);
         cmd.preservedText = null;
         cmd.event = null;
         cmd.text = null;
     }

     protected void internalRevealRange(int start, int end) {
         StyledText textWidget = getTextWidget();
         int startLine = documentAdapter.getLineAtOffset(start);
         int endLine = documentAdapter.getLineAtOffset(end);

         int top = textWidget.getTopIndex();
         if (top > -1) {
             // scroll vertically
 int lines = getVisibleLinesInViewport();
             int bottom = top + lines;

             // two lines at the top and the bottom should always be left
 // if window is smaller than 5 lines, always center position is
 // chosen
 int bufferZone = 2;
             if (startLine >= top + bufferZone && startLine <= bottom - bufferZone && endLine >= top + bufferZone && endLine <= bottom - bufferZone) {

                 // do not scroll at all as it is already visible
 } else {
                 int delta = Math.max(0, lines - (endLine - startLine));
                 textWidget.setTopIndex(startLine - delta / 3);
                 updateViewportListeners(INTERNAL);
             }

             // scroll horizontally
 if (endLine < startLine) {
                 endLine += startLine;
                 startLine = endLine - startLine;
                 endLine -= startLine;
             }

             int startPixel = -1;
             int endPixel = -1;

             if (endLine > startLine) {
                 // reveal the beginning of the range in the start line
 IRegion extent = getExtent(start, start);
                 startPixel = extent.getOffset() + textWidget.getHorizontalPixel();
                 endPixel = startPixel;
             } else {
                 IRegion extent = getExtent(start, end);
                 startPixel = extent.getOffset() + textWidget.getHorizontalPixel();
                 endPixel = startPixel + extent.getLength();
             }

             int visibleStart = textWidget.getHorizontalPixel();
             int visibleEnd = visibleStart + textWidget.getClientArea().width;

             // scroll only if not yet visible
 if (startPixel < visibleStart || visibleEnd < endPixel) {
                 // set buffer zone to 10 pixels
 bufferZone = 10;
                 int newOffset = visibleStart;
                 int visibleWidth = visibleEnd - visibleStart;
                 int selectionPixelWidth = endPixel - startPixel;

                 if (startPixel < visibleStart)
                     newOffset = startPixel;
                 else if (selectionPixelWidth + bufferZone < visibleWidth)
                     newOffset = endPixel + bufferZone - visibleWidth;
                 else
                     newOffset = startPixel;

                 float index = ((float) newOffset) / ((float) getAverageCharWidth());

                 textWidget.setHorizontalIndex(Math.round(index));
             }

         }
     }

 }

